iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 30
0

DAY29有提到一個東西 Context
Context是什麼呢?
Golang v1.7之後併入標準庫內,去google出來的context好像強國文章大部份都叫「上下文」,連翻譯叫都「上下文」,雖然上下文的意境好像有點沒錯啦,因為context主要是拿來做goroutine間的控管。

有興趣可以參考這幾篇文章
上下文 Context
深度解密Go语言之context

Goroutine對go來說是個使用上很方便,管理起來很屎尿的東西,
Goroutine可以包Goroutine,
像以下的範例,雖然很爛...

func main() {
	for i := 0; i < 10; i++ {
		ii := i
		fmt.Print("i:", ii)
		go func() {
			for j := 0; j < 20; j++ {
				jj := j
				fmt.Print("i j:", ii, jj)
				go func() {
					time.Sleep(1 * time.Second)
					fmt.Print("GG")
				}()
			}
		}()
	}
	for {
	}
}

上面的例子會同時併發200個Goroutine出去處理事情,雖然可以使用channel管理,但是一但遇到多層的併發狀態時,要進行狀態管理是很無解的,就跟KD勇一樣無解

要如何使用context呢

WithDeadline 設定context的生命週期,時間到了才會終止

package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/gin-gonic/gin"
)

func main() {
	router := gin.Default()
	router.GET("/DoGetByQueryString", func(c *gin.Context) {
		time.Sleep(20 * time.Second)
		p1 := c.DefaultQuery("param1", "Default")
		p2 := c.Query("param2")
		c.JSON(http.StatusOK, gin.H{"param1": p1, "param2": p2})
	})
	srv := &http.Server{
		Addr:    ":8787",
		Handler: router,
	}
	ch := make(chan os.Signal, 1)
	go func() {
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			fmt.Println("SERVER GG惹:", err)
		}
	}()
	signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
	<-ch
	log.Println("context.WithDeadline start:", time.Now())
    //收到關閉訊息後,設定WithDeadline為20秒後,20秒後才會發出context.Done的訊息出來
	c, cancel := context.WithDeadline(context.Background(), time.Now().Add(20*time.Second))
	defer cancel()
	if err := srv.Shutdown(c); err != nil {
		log.Println("srv.Shutdown:", err)
	}
	select {
	case <-c.Done():
		fmt.Println("context.WithDeadline done:", time.Now(), c.Err().Error())
		close(ch)
	}
}

執行結果,二個時間區間正好20秒

2020/10/06 11:26:39 context.WithDeadline start: 2020-10-06 11:26:39.157852 +0800 CST m=+8.180100057
context.WithDeadline done: 2020-10-06 11:26:59.16354 +0800 CST m=+28.185433792 context deadline exceeded

WithTimeout 設定context的生命週期,超時就發出終止訊息

把WithDeadline的code改成下面這句,5秒後就強制停止

c, cancel := context.WithTimeout(context.Background(), 5*time.Second)

執行結果,差五秒

C2020/10/06 11:31:45 context.WithTimeout start: 2020-10-06 11:31:45.65742 +0800 CST m=+4.507931447
context.WithTimeout done: 2020-10-06 11:31:50.660076 +0800 CST m=+9.510437665 context deadline exceeded

基本上WithDeadline跟WithTimeout可以視為相同的東西,只是WithDeadline設定的時間,所以要考慮到時區問題,這樣子反而使用WithTimeout還比較安全


WithCancel 設定context可以自行發出終止訊息

因為context是個樹狀的結構,主線程的Parent context可以產生子context,子context又可以繼續生成下層context,使用WithCancel產生子context時,這可以透過context.Cancel()發出終止訊息,讓繼承出來的context一起收到終止訊息

使用上面的code來範例

package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/gin-gonic/gin"
)

func main() {
	router := gin.Default()
	router.GET("/DoGetByQueryString", func(c *gin.Context) {
		time.Sleep(20 * time.Second)
		p1 := c.DefaultQuery("param1", "Default")
		p2 := c.Query("param2")
		c.JSON(http.StatusOK, gin.H{"param1": p1, "param2": p2})
	})
	srv := &http.Server{
		Addr:    ":8787",
		Handler: router,
	}

	go func() {
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			fmt.Println("SERVER GG惹:", err)
		}
	}()
    
    //產生一個屬於WithCancel的子context
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	go func() {
		ch := make(chan os.Signal, 1)
		signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
		select {
        //聽到關閉訊號就對ctx進行關閉
		case <-ch:
			log.Println("listen SIGTERM:", time.Now())
			close(ch)
			cancel()
		}
	}()
	<-ctx.Done()
    //繼承上層context產生一個WithTimeout的context,設定5秒後timeout
	log.Println("context.WithTimeout start:", time.Now())
	c, cancel := context.WithTimeout(ctx, 5*time.Second)
	defer cancel()
	if err := srv.Shutdown(c); err != nil {
		log.Println("srv.Shutdown:", err)
	}
	select {
	case <-c.Done():
		fmt.Println("context.WithTimeout done:", time.Now(), c.Err().Error())
	}
}

執行結果:因為上層的context聽到關閉訊號,馬上對下層context發出context.Done(),這時候就算是使用WithTimeout context的srv.Shutdown也會跟著被影響到,馬上進行關閉

2020/10/06 12:05:25 listen SIGTERM: 2020-10-06 12:05:25.046776 +0800 CST m=+5.484417516
2020/10/06 12:05:25 context.WithTimeout start: 2020-10-06 12:05:25.047202 +0800 CST m=+5.484843131
//srv.Shutdown()發生錯誤了
2020/10/06 12:05:25 srv.Shutdown: context canceled
context.WithTimeout done: 2020-10-06 12:05:25.047311 +0800 CST m=+5.484952722 context canceled

WithValue context裡面塞值

因為context是thread safe,所以Goroutine間對context進行WithValue是不會噴data race的,
WithValue主要是做值的傳遞使用
雖然參數2定義上是interface{},但是使用string當key值時,go-lint會提示,執行上還是沒問題

should not use basic type string as key in context.WithValue

參考官方寫法的話,先自定義一個type,再宣告一個自定義type的變數來當key值

type contextKey string
key := contextKey("test")
ctx := context.WithValue(context.Background(), key, "i'm test")
fmt.Println(ctx.Value(key))

WithValue產生的context再產生下層context時,會把塞好的value傳遞給下層context中

type contextKey string
key := contextKey("test")
//產生一個context帶有{"test":"i'm test"}的值
ctx := context.WithValue(context.Background(), key, "i'm test")
fmt.Println(ctx.Value(key))
//繼承ctx產生一個子context
ctx1, cancel := context.WithCancel(ctx)
defer cancel()
fmt.Println(ctx1.Value(key))

執行結果,驗證了透過WithValue可以把值傳遞給context衍生出來子context,好像拿來做trace_id使用應該不錯,每次的request都是一個context,如果用這方式就log這個request的足跡

i'm test
i'm test

context的使用原則

原文在這 Overview第五段

  • 不要把Context放在struct裡,請使用參數
  • 新增func時,如果context要當其中的參數時,要把context放在第一個參數,然後名稱叫ctx。
func DoSomethingWrong(ctx context.Context) error {
	
}
  • 如果參數中有context的話,不要傳nil阿,可以改用context.TODO(),這會產一個空的context
  • Context裡面要塞value的話,請確認是很重要的值,不要什麼鬼都用context傳遞
  • Context是thread safe,可以在多個Goroutine中使用,不會噴data race

鐵人賽30天小小心得

Golang是第一次接觸的程式語言,雖然有些地方在其他程式也有看到,但是感覺Golang就是一個更輕量化的程式語言(望著c#),要寫個http的服務真的程式少少幾行就可以run起來,加上萬惡的Goroutine,用過之後就回不去了。
寫了鐵人賽之後才回看去看看Gin與Mux的middleware在幹嘛,原來套件已經提供了很好的middleware func可以直接使用,不需要再自己硬刻code。
雖然寫30天真的很硬又很血汗,一邊工作在爆,還要回家刻文章,想想還真的頗痛苦,但是說真的,寫完才知道之前不懂的東西,居然在寫完文章後就最少能懂個皮毛,算是參賽最大的收獲吧,也感謝寫Golang文章的前輩,拜讀完才能促使自己寫下文章,文章還有很多不足的地方,也請閱讀的人請多多包涵~~感謝~


上一篇
[DAY29]Golang Graceful Shutdown優雅的關機(?)
系列文
欸你這週GO了嘛30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言